// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package google

import (
	"context"
	"fmt"
	"strings"
	"time"

	"yunion.io/x/jsonutils"
	"yunion.io/x/log"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/pkg/util/billing"
	"yunion.io/x/pkg/util/cloudinit"
	"yunion.io/x/pkg/util/encode"
	"yunion.io/x/pkg/util/fileutils"
	"yunion.io/x/pkg/util/imagetools"
	"yunion.io/x/pkg/util/stringutils"
	"yunion.io/x/pkg/utils"

	billing_api "yunion.io/x/cloudmux/pkg/apis/billing"
	api "yunion.io/x/cloudmux/pkg/apis/compute"
	"yunion.io/x/cloudmux/pkg/cloudprovider"
	"yunion.io/x/cloudmux/pkg/multicloud"
)

const (
	METADATA_SSH_KEYS                   = "ssh-keys"
	METADATA_STARTUP_SCRIPT             = "startup-script"
	METADATA_POWER_SHELL                = "sysprep-specialize-script-ps1"
	METADATA_STARTUP_SCRIPT_POWER_SHELL = "windows-startup-script-ps1"
)

type AccessConfig struct {
	Type        string
	Name        string
	NatIP       string
	NetworkTier string
	Kind        string
}

type InstanceDisk struct {
	Type            string
	Mode            string
	Source          string
	DeviceName      string
	Index           int
	Boot            bool
	AutoDelete      bool
	Licenses        []string
	Interface       string
	GuestOsFeatures []GuestOsFeature
	Kind            string
}

type ServiceAccount struct {
	Email  string
	scopes []string
}

type SInstanceTag struct {
	Items       []string
	Fingerprint string
}

type SMetadataItem struct {
	Key   string
	Value string
}

type SMetadata struct {
	Fingerprint string
	Items       []SMetadataItem
}

type SInstance struct {
	multicloud.SInstanceBase
	GoogleTags
	host *SHost
	SResourceBase

	osInfo *imagetools.ImageInfo

	CreationTimestamp  time.Time
	Description        string
	Tags               SInstanceTag
	MachineType        string
	Status             string
	Zone               string
	CanIpForward       bool
	NetworkInterfaces  []SNetworkInterface
	Disks              []InstanceDisk
	Metadata           SMetadata
	ServiceAccounts    []ServiceAccount
	Scheduling         map[string]interface{}
	CpuPlatform        string
	LabelFingerprint   string
	StartRestricted    bool
	DeletionProtection bool
	Kind               string

	guestCpus   int
	memoryMb    int
	machineType string
}

func (region *SRegion) GetInstances(zone string, maxResults int, pageToken string) ([]SInstance, error) {
	instances := []SInstance{}
	params := map[string]string{}
	if len(zone) == 0 {
		return nil, fmt.Errorf("zone params can not be empty")
	}
	resource := fmt.Sprintf("zones/%s/instances", zone)
	return instances, region.List(resource, params, maxResults, pageToken, &instances)
}

func (region *SRegion) GetInstance(id string) (*SInstance, error) {
	instance := &SInstance{}
	return instance, region.Get("instances", id, instance)
}

func (instance *SInstance) GetHostname() string {
	return ""
}

func (instance *SInstance) fetchMachineType() error {
	if instance.guestCpus > 0 || instance.memoryMb > 0 || len(instance.machineType) > 0 {
		return nil
	}
	machinetype := SMachineType{}
	err := instance.host.zone.region.GetBySelfId(instance.MachineType, &machinetype)
	if err != nil {
		return err
	}
	instance.guestCpus = machinetype.GuestCpus
	instance.memoryMb = machinetype.MemoryMb
	instance.machineType = machinetype.Name
	return nil
}

func (self *SInstance) Refresh() error {
	instance, err := self.host.zone.region.GetInstance(self.Id)
	if err != nil {
		return err
	}
	err = jsonutils.Update(self, instance)
	if err != nil {
		return err
	}
	instance.Labels = self.Labels
	return nil
}

// PROVISIONING, STAGING, RUNNING, STOPPING, STOPPED, SUSPENDING, SUSPENDED, and TERMINATED.
func (instance *SInstance) GetStatus() string {
	switch instance.Status {
	case "PROVISIONING":
		return api.VM_DEPLOYING
	case "STAGING":
		return api.VM_STARTING
	case "RUNNING":
		return api.VM_RUNNING
	case "STOPPING":
		return api.VM_STOPPING
	case "STOPPED":
		return api.VM_READY
	case "SUSPENDING":
		return api.VM_SUSPENDING
	case "SUSPENDED":
		return api.VM_SUSPEND
	case "TERMINATED":
		return api.VM_READY
	default:
		return api.VM_UNKNOWN
	}
}

func (ins *SInstance) GetPowerStates() string {
	status := ins.GetStatus()
	switch status {
	case api.VM_READY:
		return api.VM_POWER_STATES_OFF
	case api.VM_UNKNOWN:
		return api.VM_POWER_STATES_OFF
	default:
		return api.VM_POWER_STATES_ON
	}
}

func (instance *SInstance) GetBillingType() string {
	return billing_api.BILLING_TYPE_POSTPAID
}

func (instance *SInstance) GetCreatedAt() time.Time {
	return instance.CreationTimestamp
}

func (instance *SInstance) GetExpiredAt() time.Time {
	return time.Time{}
}

func (instance *SInstance) GetProjectId() string {
	return instance.host.zone.region.GetProjectId()
}

func (instance *SInstance) GetIHost() cloudprovider.ICloudHost {
	return instance.host
}

func (instance *SInstance) GetIHostId() string {
	return instance.host.GetGlobalId()
}

func (instance *SInstance) GetIDisks() ([]cloudprovider.ICloudDisk, error) {
	idisks := []cloudprovider.ICloudDisk{}
	for _, disk := range instance.Disks {
		_disk := &SDisk{}
		err := instance.host.zone.region.GetBySelfId(disk.Source, _disk)
		if err != nil {
			return nil, errors.Wrap(err, "GetDisk")
		}
		storage, err := instance.host.zone.region.GetStorage(_disk.Type)
		if err != nil {
			return nil, errors.Wrap(err, "GetStorage")
		}
		storage.zone = instance.host.zone
		_disk.storage = storage
		_disk.autoDelete = disk.AutoDelete
		_disk.boot = disk.Boot
		_disk.index = disk.Index
		idisks = append(idisks, _disk)
	}
	return idisks, nil
}

func (instance *SInstance) GetINics() ([]cloudprovider.ICloudNic, error) {
	nics := []cloudprovider.ICloudNic{}
	for i := range instance.NetworkInterfaces {
		instance.NetworkInterfaces[i].instance = instance
		nics = append(nics, &instance.NetworkInterfaces[i])
	}
	return nics, nil
}

func (instance *SInstance) GetIEIP() (cloudprovider.ICloudEIP, error) {
	for _, networkinterface := range instance.NetworkInterfaces {
		for _, conf := range networkinterface.AccessConfigs {
			if len(conf.NatIP) > 0 {
				eips, err := instance.host.zone.region.GetEips(conf.NatIP, 0, "")
				if err != nil {
					return nil, errors.Wrapf(err, "region.GetEip(%s)", conf.NatIP)
				}
				if len(eips) == 1 {
					eips[0].region = instance.host.zone.region
					return &eips[0], nil
				}
				eip := &SAddress{
					region:     instance.host.zone.region,
					Status:     "IN_USE",
					Address:    conf.NatIP,
					instanceId: instance.Id,
				}
				eip.Id = instance.Id
				eip.SelfLink = instance.SelfLink
				return eip, nil
			}
		}
	}
	return nil, nil
}

func (instance *SInstance) GetVcpuCount() int {
	instance.fetchMachineType()
	return instance.guestCpus
}

func (instance *SInstance) GetVmemSizeMB() int {
	instance.fetchMachineType()
	return instance.memoryMb
}

func (instance *SInstance) GetBootOrder() string {
	return "cdn"
}

func (instance *SInstance) GetVga() string {
	return "std"
}

func (instance *SInstance) GetVdi() string {
	return "vnc"
}

func (instance *SInstance) GetOsType() cloudprovider.TOsType {
	return cloudprovider.TOsType(instance.getNormalizedOsInfo().OsType)
}

func (instance *SInstance) getValidLicense() string {
	for _, disk := range instance.Disks {
		if disk.Index == 0 {
			for _, license := range disk.Licenses {
				if len(license) > 0 {
					return license
				}
			}
		}
	}
	return ""
}

func (instance *SInstance) getNormalizedOsInfo() *imagetools.ImageInfo {
	if instance.osInfo != nil {
		return instance.osInfo
	}
	osinfo := imagetools.NormalizeImageInfo(instance.getValidLicense(), "", "", "", "")
	instance.osInfo = &osinfo
	return instance.osInfo
}

func (instance *SInstance) GetFullOsName() string {
	return instance.getValidLicense()
}

func (instance *SInstance) GetBios() cloudprovider.TBiosType {
	return cloudprovider.ToBiosType(instance.getNormalizedOsInfo().OsBios)
}

func (instance *SInstance) GetOsArch() string {
	return instance.getNormalizedOsInfo().OsArch
}

func (instance *SInstance) GetOsDist() string {
	return instance.getNormalizedOsInfo().OsDistro
}

func (instance *SInstance) GetOsVersion() string {
	return instance.getNormalizedOsInfo().OsVersion
}

func (instance *SInstance) GetOsLang() string {
	return instance.getNormalizedOsInfo().OsLang
}

func (instance *SInstance) GetMachine() string {
	return "pc"
}

func (instance *SInstance) GetInstanceType() string {
	instance.fetchMachineType()
	return instance.machineType
}

func (instance *SInstance) GetSecurityGroupIds() ([]string, error) {
	return instance.Tags.Items, nil
}

func (instance *SInstance) SetSecurityGroups(ids []string) error {
	instance.Tags.Items = ids
	return instance.host.zone.region.SetResourceTags(instance.SelfLink, instance.Tags)
}

func (instance *SInstance) GetHypervisor() string {
	return api.HYPERVISOR_GOOGLE
}

func (instance *SInstance) StartVM(ctx context.Context) error {
	return instance.host.zone.region.StartInstance(instance.SelfLink)
}

func (instance *SInstance) StopVM(ctx context.Context, opts *cloudprovider.ServerStopOptions) error {
	return instance.host.zone.region.StopInstance(instance.SelfLink)
}

func (instance *SInstance) DeleteVM(ctx context.Context) error {
	if instance.DeletionProtection {
		err := instance.host.zone.region.DisableDeletionProtection(instance.SelfLink)
		if err != nil {
			return errors.Wrapf(err, "DisableDeletionProtection(%s)", instance.Name)
		}
	}
	return instance.host.zone.region.Delete(instance.SelfLink)
}

func (instance *SInstance) UpdateVM(ctx context.Context, input cloudprovider.SInstanceUpdateOptions) error {
	return cloudprovider.ErrNotSupported
}

func (instance *SInstance) UpdateUserData(userData string) error {
	items := []SMetadataItem{}
	for _, item := range instance.Metadata.Items {
		if item.Key != METADATA_STARTUP_SCRIPT && item.Key != METADATA_POWER_SHELL && item.Key != METADATA_STARTUP_SCRIPT_POWER_SHELL {
			items = append(items, item)
		}
	}
	if len(userData) > 0 {
		items = append(items, SMetadataItem{Key: METADATA_STARTUP_SCRIPT, Value: userData})
		items = append(items, SMetadataItem{Key: METADATA_STARTUP_SCRIPT_POWER_SHELL, Value: userData})
		items = append(items, SMetadataItem{Key: METADATA_POWER_SHELL, Value: userData})
	}
	instance.Metadata.Items = items
	return instance.host.zone.region.SetMetadata(instance.SelfLink, instance.Metadata)
}

func (instance *SInstance) RebuildRoot(ctx context.Context, opts *cloudprovider.SManagedVMRebuildRootConfig) (string, error) {
	diskId, err := instance.host.zone.region.RebuildRoot(instance.Id, opts.ImageId, opts.SysSizeGB)
	if err != nil {
		return "", errors.Wrap(err, "region.RebuildRoot")
	}
	deployOpts := &cloudprovider.SInstanceDeployOptions{
		Username:  opts.Account,
		Password:  opts.Password,
		UserData:  opts.UserData,
		PublicKey: opts.PublicKey,
	}
	return diskId, instance.DeployVM(ctx, deployOpts)
}

func (instance *SInstance) DeployVM(ctx context.Context, opts *cloudprovider.SInstanceDeployOptions) error {
	conf := cloudinit.SCloudConfig{
		SshPwauth: cloudinit.SSH_PASSWORD_AUTH_ON,
	}
	if len(opts.UserData) > 0 {
		config, err := cloudinit.ParseUserData(opts.UserData)
		if err == nil {
			conf.Merge(config)
		}
	}
	user := cloudinit.NewUser(opts.Username)
	if len(opts.Password) > 0 {
		user.Password(opts.Password)
	}
	if len(opts.PublicKey) > 0 {
		user.SshKey(opts.PublicKey)
	}
	if len(opts.Password) > 0 || len(opts.PublicKey) > 0 {
		conf.MergeUser(user)
		items := []SMetadataItem{}
		instance.Refresh()
		for _, item := range instance.Metadata.Items {
			if item.Key != METADATA_STARTUP_SCRIPT_POWER_SHELL && item.Key != METADATA_STARTUP_SCRIPT {
				items = append(items, item)
			}
		}
		items = append(items, SMetadataItem{Key: METADATA_STARTUP_SCRIPT_POWER_SHELL, Value: conf.UserDataPowerShell()})
		items = append(items, SMetadataItem{Key: METADATA_STARTUP_SCRIPT, Value: conf.UserDataScript()})
		instance.Metadata.Items = items
		return instance.host.zone.region.SetMetadata(instance.SelfLink, instance.Metadata)
	}
	if opts.DeleteKeypair {
		items := []SMetadataItem{}
		items = append(items, SMetadataItem{Key: METADATA_STARTUP_SCRIPT, Value: cloudinit.CLOUD_SHELL_HEADER + "\nrm -rf /root/.ssh/authorized_keys"})
		instance.Refresh()
		for _, item := range instance.Metadata.Items {
			if item.Key != METADATA_STARTUP_SCRIPT {
				items = append(items, item)
			}
		}
		instance.Metadata.Items = items
		return instance.host.zone.region.SetMetadata(instance.SelfLink, instance.Metadata)
	}
	return nil
}

func (instance *SInstance) ChangeConfig(ctx context.Context, config *cloudprovider.SManagedVMChangeConfig) error {
	return instance.host.zone.region.ChangeInstanceConfig(instance.SelfLink, instance.host.zone.Name, config.InstanceType, config.Cpu, config.MemoryMB)
}

func (instance *SInstance) GetVNCInfo(input *cloudprovider.ServerVncInput) (*cloudprovider.ServerVncOutput, error) {
	return nil, cloudprovider.ErrNotImplemented
}

func (instance *SInstance) AttachDisk(ctx context.Context, diskId string) error {
	return instance.host.zone.region.AttachDisk(instance.SelfLink, diskId, false)
}

func (instance *SInstance) DetachDisk(ctx context.Context, diskId string) error {
	_disk, err := instance.host.zone.region.GetDisk(diskId)
	if err != nil {
		if errors.Cause(err) == cloudprovider.ErrNotFound {
			return nil
		}
		return errors.Wrapf(err, "GetDisk(%s)", diskId)
	}
	for _, disk := range instance.Disks {
		if disk.Source == _disk.SelfLink {
			return instance.host.zone.region.DetachDisk(instance.SelfLink, disk.DeviceName)
		}
	}
	return nil
}

func (instance *SInstance) Renew(bc billing.SBillingCycle) error {
	return cloudprovider.ErrNotSupported
}

func (instance *SInstance) GetError() error {
	return nil
}

func getDiskInfo(disk string) (cloudprovider.SDiskInfo, error) {
	result := cloudprovider.SDiskInfo{}
	diskInfo := strings.Split(disk, ":")
	for _, d := range diskInfo {
		if utils.IsInStringArray(d, []string{
			api.STORAGE_GOOGLE_PD_STANDARD, api.STORAGE_GOOGLE_PD_SSD, api.STORAGE_GOOGLE_LOCAL_SSD, api.STORAGE_GOOGLE_PD_BALANCED, api.STORAGE_GOOGLE_PD_EXTREME,
			api.STORAGE_GOOGLE_HYPERDISK_THROUGHPUT, api.STORAGE_GOOGLE_HYPERDISK_ML, api.STORAGE_GOOGLE_HYPERDISK_BALANCED, api.STORAGE_GOOGLE_HYPERDISK_EXTREME}) {
			result.StorageType = d
		} else if memSize, err := fileutils.GetSizeMb(d, 'M', 1024); err == nil {
			result.SizeGB = memSize >> 10
		} else {
			result.Name = d
		}
	}
	if len(result.StorageType) == 0 {
		result.StorageType = api.STORAGE_GOOGLE_PD_STANDARD
	}

	if result.SizeGB == 0 {
		return result, fmt.Errorf("missing disk size")
	}
	return result, nil
}

func (region *SRegion) CreateInstance(zone, name, desc, instanceType string, cpu, memoryMb int, networkId string, ipAddr, imageId string, disks []string) (*SInstance, error) {
	if len(instanceType) == 0 && (cpu == 0 || memoryMb == 0) {
		return nil, fmt.Errorf("missing instanceType or cpu &memory info")
	}
	if len(disks) == 0 {
		return nil, fmt.Errorf("missing disk info")
	}
	sysDisk, err := getDiskInfo(disks[0])
	if err != nil {
		return nil, errors.Wrap(err, "getDiskInfo.sys")
	}
	dataDisks := []cloudprovider.SDiskInfo{}
	for _, d := range disks[1:] {
		dataDisk, err := getDiskInfo(d)
		if err != nil {
			return nil, errors.Wrapf(err, "getDiskInfo(%s)", d)
		}
		dataDisks = append(dataDisks, dataDisk)
	}
	conf := &cloudprovider.SManagedVMCreateConfig{
		Name:              name,
		Description:       desc,
		ExternalImageId:   imageId,
		Cpu:               cpu,
		MemoryMB:          memoryMb,
		ExternalNetworkId: networkId,
		IpAddr:            ipAddr,
		SysDisk:           sysDisk,
		DataDisks:         dataDisks,
	}
	return region._createVM(zone, conf)
}

func (region *SRegion) _createVM(zone string, desc *cloudprovider.SManagedVMCreateConfig) (*SInstance, error) {
	if len(desc.InstanceType) == 0 {
		desc.InstanceType = fmt.Sprintf("custom-%d-%d", desc.Cpu, desc.MemoryMB)
	}
	disks := []map[string]interface{}{}
	if len(desc.SysDisk.Name) == 0 {
		desc.SysDisk.Name = fmt.Sprintf("vdisk-%s-%d", desc.Name, time.Now().UnixNano())
	}

	labels := map[string]string{}
	for k, v := range desc.Tags {
		labels[encode.EncodeGoogleLabel(k)] = encode.EncodeGoogleLabel(v)
	}

	disks = append(disks, map[string]interface{}{
		"boot": true,
		"initializeParams": map[string]interface{}{
			"diskName":    normalizeString(desc.SysDisk.Name),
			"sourceImage": desc.ExternalImageId,
			"diskSizeGb":  desc.SysDisk.SizeGB,
			"diskType":    fmt.Sprintf("zones/%s/diskTypes/%s", zone, desc.SysDisk.StorageType),
			"labels":      labels,
		},
		"autoDelete": true,
	})
	for _, disk := range desc.DataDisks {
		if len(disk.Name) == 0 {
			disk.Name = fmt.Sprintf("vdisk-%s-%d", desc.Name, time.Now().UnixNano())
		}
		disks = append(disks, map[string]interface{}{
			"boot": false,
			"initializeParams": map[string]interface{}{
				"diskName":   normalizeString(disk.Name),
				"diskSizeGb": disk.SizeGB,
				"diskType":   fmt.Sprintf("zones/%s/diskTypes/%s", zone, disk.StorageType),
				"labels":     labels,
			},
			"autoDelete": true,
		})
	}

	networkInterface := map[string]string{
		"subnetwork": desc.ExternalNetworkId,
	}
	if !strings.HasPrefix(desc.ExternalNetworkId, "projects/") {
		vpc, err := region.GetVpc(desc.ExternalNetworkId)
		if err != nil {
			return nil, errors.Wrap(err, "region.GetNetwork")
		}
		networkInterface["subnetwork"] = getGlobalId(vpc.SelfLink)
	}
	if len(desc.IpAddr) > 0 {
		networkInterface["networkIp"] = desc.IpAddr
	}
	params := map[string]interface{}{
		"name":        normalizeString(desc.NameEn),
		"description": desc.Description,
		"machineType": fmt.Sprintf("zones/%s/machineTypes/%s", zone, desc.InstanceType),
		"networkInterfaces": []map[string]string{
			networkInterface,
		},
		"disks": disks,
	}

	if len(labels) > 0 {
		params["labels"] = labels
	}

	if len(desc.ExternalSecgroupIds) > 0 {
		params["tags"] = map[string][]string{
			"items": desc.ExternalSecgroupIds,
		}
	}

	if len(desc.UserData) > 0 {
		params["metadata"] = map[string]interface{}{
			"items": []struct {
				Key   string
				Value string
			}{
				{
					Key:   METADATA_STARTUP_SCRIPT,
					Value: desc.UserData,
				},
				{
					Key:   METADATA_POWER_SHELL,
					Value: desc.UserData,
				},
			},
		}
	}
	log.Debugf("create google instance params: %s", jsonutils.Marshal(params).String())
	instance := &SInstance{}
	resource := fmt.Sprintf("zones/%s/instances", zone)
	err := region.Insert(resource, jsonutils.Marshal(params), instance)
	if err != nil {
		return nil, err
	}
	return instance, nil
}

func (region *SRegion) StartInstance(id string) error {
	params := map[string]string{}
	return region.Do(id, "start", nil, jsonutils.Marshal(params))
}

func (region *SRegion) StopInstance(id string) error {
	params := map[string]string{}
	return region.Do(id, "stop", nil, jsonutils.Marshal(params))
}

func (region *SRegion) ResetInstance(id string) error {
	params := map[string]string{}
	return region.Do(id, "reset", nil, jsonutils.Marshal(params))
}

func (region *SRegion) DisableDeletionProtection(id string) error {
	params := map[string]string{
		"requestId":          stringutils.UUID4(),
		"deletionProtection": "false",
	}
	return region.Do(id, "setDeletionProtection", params, nil)
}

func (region *SRegion) DetachDisk(instanceId, deviceName string) error {
	body := map[string]string{}
	params := map[string]string{"deviceName": deviceName}
	return region.Do(instanceId, "detachDisk", params, jsonutils.Marshal(body))
}

func (instance *SInstance) GetSerialOutput(port int) (string, error) {
	return instance.host.zone.region.GetSerialPortOutput(instance.SelfLink, port)
}

func (region *SRegion) GetSerialPortOutput(id string, port int) (string, error) {
	_content, content, next := "", "", 0
	var err error = nil
	for {
		_content, next, err = region.getSerialPortOutput(id, port, next)
		if err != nil {
			return content, err
		}
		content += _content
		if len(_content) == 0 {
			break
		}
	}
	return content, nil
}

func (region *SRegion) getSerialPortOutput(id string, port int, start int) (string, int, error) {
	resource := fmt.Sprintf("%s/serialPort?port=%d&start=%d", id, port, start)
	result := struct {
		Contents string
		Start    int
		Next     int
	}{}
	err := region.GetBySelfId(resource, &result)
	if err != nil {
		return "", result.Next, errors.Wrap(err, "")
	}
	return result.Contents, result.Next, nil
}

func (self *SRegion) AttachDisk(instanceId, diskId string, boot bool) error {
	disk, err := self.GetDisk(diskId)
	if err != nil {
		return errors.Wrapf(err, "GetDisk(%s)", diskId)
	}
	body := map[string]interface{}{
		"source": disk.SelfLink,
		"boot":   boot,
	}
	if boot {
		body["autoDelete"] = true
	}
	params := map[string]string{}
	return self.Do(instanceId, "attachDisk", params, jsonutils.Marshal(body))
}

func (region *SRegion) ChangeInstanceConfig(id string, zone string, instanceType string, cpu int, memoryMb int) error {
	if len(instanceType) == 0 {
		instanceType = fmt.Sprintf("custom-%d-%d", cpu, memoryMb)
	}
	params := map[string]string{
		"machineType": fmt.Sprintf("zones/%s/machineTypes/%s", zone, instanceType),
	}
	return region.Do(id, "setMachineType", nil, jsonutils.Marshal(params))
}

func (region *SRegion) SetMetadata(id string, metadata SMetadata) error {
	return region.Do(id, "setMetadata", nil, jsonutils.Marshal(metadata))
}

func (region *SRegion) SetResourceTags(id string, tags SInstanceTag) error {
	return region.Do(id, "setTags", nil, jsonutils.Marshal(tags))
}

func (region *SRegion) SetServiceAccount(id string, email string) error {
	body := map[string]interface{}{
		"email": email,
		"scopes": []string{
			"https://www.googleapis.com/auth/devstorage.read_only",
			"https://www.googleapis.com/auth/logging.write",
			"https://www.googleapis.com/auth/monitoring.write",
			"https://www.googleapis.com/auth/servicecontrol",
			"https://www.googleapis.com/auth/service.management.readonly",
			"https://www.googleapis.com/auth/trace.append",
		},
	}
	return region.Do(id, "setsetServiceAccount", nil, jsonutils.Marshal(body))
}

func (region *SRegion) RebuildRoot(instanceId string, imageId string, sysDiskSizeGb int) (string, error) {
	oldDisk, diskType, deviceName := "", api.STORAGE_GOOGLE_PD_STANDARD, ""
	instance, err := region.GetInstance(instanceId)
	if err != nil {
		return "", errors.Wrap(err, "region.GetInstance")
	}
	for _, disk := range instance.Disks {
		if disk.Boot {
			oldDisk = disk.Source
			deviceName = disk.DeviceName
			break
		}
	}

	if len(oldDisk) > 0 {
		disk := &SDisk{}
		err := region.GetBySelfId(oldDisk, disk)
		if err != nil {
			return "", errors.Wrap(err, "region.GetDisk")
		}
		diskType = disk.Type
		if sysDiskSizeGb == 0 {
			sysDiskSizeGb = disk.SizeGB
		}
	}
	image, err := region.GetImage(imageId)
	if err != nil {
		return "", errors.Wrapf(err, "GetImage")
	}
	if image.DiskSizeGb > sysDiskSizeGb {
		sysDiskSizeGb = image.DiskSizeGb
	}

	zone, err := region.GetZone(instance.Zone)
	if err != nil {
		return "", errors.Wrap(err, "region.GetZone")
	}

	diskName := fmt.Sprintf("vdisk-%s-%d", instance.Name, time.Now().UnixNano())
	disk, err := region.CreateDisk(zone.Name, diskType, &cloudprovider.DiskCreateConfig{
		Name:    diskName,
		SizeGb:  sysDiskSizeGb,
		ImageId: imageId,
		Desc:    "create for replace instance system disk",
	})
	if err != nil {
		return "", errors.Wrap(err, "region.CreateDisk.systemDisk")
	}

	if len(deviceName) > 0 {
		err = region.DetachDisk(instance.SelfLink, deviceName)
		if err != nil {
			defer region.Delete(disk.SelfLink)
			return "", errors.Wrap(err, "region.DetachDisk")
		}
	}

	err = region.AttachDisk(instance.SelfLink, disk.Id, true)
	if err != nil {
		if len(oldDisk) > 0 {
			defer region.AttachDisk(instance.SelfLink, oldDisk, true)
		}
		defer region.Delete(disk.SelfLink)
		return "", errors.Wrap(err, "region.AttachDisk.newSystemDisk")
	}

	if len(oldDisk) > 0 {
		defer region.Delete(oldDisk)
	}
	return disk.GetGlobalId(), nil
}

func (self *SRegion) SaveImage(diskId string, opts *cloudprovider.SaveImageOptions) (*SImage, error) {
	params := map[string]interface{}{
		"name":        normalizeString(opts.Name),
		"description": opts.Notes,
		"sourceDisk":  diskId,
	}
	image := &SImage{}
	err := self.Insert("global/images", jsonutils.Marshal(params), image)
	if err != nil {
		return nil, errors.Wrapf(err, "Insert")
	}
	image.storagecache = self.getStoragecache()
	return image, nil
}

func (self *SInstance) SaveImage(opts *cloudprovider.SaveImageOptions) (cloudprovider.ICloudImage, error) {
	for i := range self.Disks {
		if self.Disks[0].Index == 0 {
			image, err := self.host.zone.region.SaveImage(self.Disks[i].Source, opts)
			if err != nil {
				return nil, errors.Wrapf(err, "SaveImage")
			}
			return image, nil
		}
	}
	return nil, errors.Wrapf(cloudprovider.ErrNotFound, "no valid system disk found")
}

func (region *SRegion) SetLabels(id string, _labels map[string]string, labelFingerprint string) error {
	labels := map[string]string{}
	for k, v := range _labels {
		labels[encode.EncodeGoogleLabel(k)] = encode.EncodeGoogleLabel(v)
	}
	params := map[string]interface{}{
		"labels":           labels,
		"labelFingerprint": labelFingerprint,
	}
	err := region.Do(id, "setLabels", nil, jsonutils.Marshal(params))
	if err != nil {
		return errors.Wrapf(err, `region.Do(%s, "setLabels", nil, %s)`, id, jsonutils.Marshal(params).String())
	}
	return nil
}

func (self *SInstance) SetTags(tags map[string]string, replace bool) error {
	if !replace {
		oldTags, _ := self.GetTags()
		for k, v := range oldTags {
			if _, ok := tags[k]; !ok {
				tags[k] = v
			}
		}
	}
	err := self.Refresh()
	if err != nil {
		return errors.Wrap(err, "self.Refresh()")
	}
	err = self.host.zone.region.SetLabels(self.SelfLink, tags, self.LabelFingerprint)
	if err != nil {
		return errors.Wrapf(err, ` self.host.zone.region.SsetLabels()`)
	}
	return nil
}
